package org.bhf.json;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Public entry point for writing JSON.
 * 
 * <p>Note: It is highly recommended that you use buffered writers for better performance</p>
 */
public final class JSONWriter
{
    private Set<String> excludes = new HashSet<String>();

    /**
     * Write a null JSON value. For convenience.
     *
     * @param out Must not be <code>null</code>
     * @throws IOException Write error
     * @exclude
     */
    public void         writeNull( final Writer out )
        throws IOException
    {
        NativeJSONWriters.writeNull( out );
    }

    /**
     * Write a <code>String</code> as a properly escaped and quoted JSON value. For convenience.
     *
     * @param string May be <code>null</code>
     * @param out Must not be <code>null</code>
     * @throws IOException Write error
     * @exclude
     */
    public void         write( final String string, final Writer out )
        throws IOException
    {
        NativeJSONWriters.write( string, out, null );
    }

    /**
     * For internal use only
     * @param key Class name + . + property
     * @return true or false
     * @exclude
     */
    public boolean      exclude( final String key )
    {
        return excludes.contains( key );
    }

    /**
     * Add a property that should be excluded from JSON marshalling.
     *
     * @param declaringClass Class on which the property is declared
     * @param property Name of property (not the method name)
     * @return The <code>JSONwriter</code> for chaining
     */
    public JSONWriter   excludeProperty( final Class declaringClass, final String property )
    {
        // TODO Fail fast with method check
        excludes.add(
            declaringClass.getName() + "." + property
        );
        return this;
    }

    /**
     * Add properties that should be excluded from JSON marshalling.
     *
     * @param declaringClass Class on which the properties are declared
     * @param properties Name of properties (not the method names)
     * @return The <code>JSONwriter</code> for chaining
     */
    public JSONWriter   excludeProperties( final Class declaringClass, final String[] properties )
    {
        for( final String property : properties )
            excludeProperty( declaringClass, property );
        return this;
    }

    /**
     * Write a general object, which could be an array, collection, or POJO with a state set of 0. This method is used
     * when the type of the object is not known at compile time and must be resolved at runtime. Based on
     * runtime information, the operation will be delegated to one of the other methods of this class.
     *
     * @param object May be <ocde>null</code>
     * @return JSON text
     * @throws IOException Write error
     */
    public String       writeObject( final Object object )
        throws IOException
    {
        final StringWriter out = new StringWriter( );
        writeObject( object, out );
        return out.toString();
    }

    /**
     * Write a general object, which could be an array, collection, or POJO using a state set of 0. This method is used
     * when the type of the object is not known at compile time and must be resolved at runtime. Based on
     * runtime information, the operation will be delegated to one of the other methods of this class.
     *
     * @param object May be <ocde>null</code>
     * @param out Must not be <code>null</code>
     * @throws IOException Write error
     */
    // Note that the method name cannot be just 'write' as the code generator has a bug and becomes
    // confused when method are overloaded with polymorpic types
    public void         writeObject( final Object object, final Writer out )
        throws IOException
    {
        if( object == null ) { NativeJSONWriters.writeNull( out ); return; }
        if( object.getClass().equals( Object.class ) ) { out.write( "{}" ); return; }

        final Method writer = getJSONWriterFor( object.getClass() );

        try
        {
            writer.invoke( null, object, out, this );
        }
        catch( IllegalAccessException e )
        {
            throw new RuntimeException( e );
        }
        catch( InvocationTargetException e )
        {
            throw new RuntimeException( e );
        }
    }

    /**
     * Convenience method to get a <code>Writer</code> for a file.
     *
     * @param file Must not be <code>null</code>
     * @param charSet Must not be <code>null</code>
     * @return Will not be <code>null</code>
     * @throws IOException I/O Error
     */
    public Writer       getFileWriter( final File file, final String charSet )
        throws IOException
    {
        return new OutputStreamWriter( new FileOutputStream( file ), charSet );
    }

    //
    // Private
    //

    private final static Map<Type,Method> writers = new HashMap<Type,Method>();

    private Method  getJSONWriterFor( final Class type )
        throws IOException
    {
        Method writerMethod;

        synchronized( writers )
        {
            writerMethod = writers.get( type );
            if( writerMethod == null )
            {
                final JSONWriterGenerator   generator       = new JSONWriterGenerator();
                final String                writerClassName = generator.getWriter( type );

                writerMethod = generator.resolveWriter( type, writerClassName );

                writers.put( type, writerMethod );
            }
        }

        return writerMethod;
    }
}
